package com.aldobucchi.ds.container.impl
{
	

	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import com.aldobucchi.core.comparison.result.ComparisonResult;
	import com.aldobucchi.ds.DataStructureEvent;
	import com.aldobucchi.ds.container.ContainerDiff;
	import com.aldobucchi.ds.container.IOContainer;
	import com.aldobucchi.ds.container.utils.ContainerUtil;
	import com.aldobucchi.ds.container.utils.MultiplicityChange;


	/**
	 * 
	 * Provides a filtered view over an observable set.
	 * Contrary to the dynamic operators, where flexibility is number one priority,
	 * this implementation focuses on memory and performance.
	 * No internal storage is created. Methods and events are proxied.
	 * 
	 * @author aldo.bucchi@gmail.com
	 * 
	 */
	public class AbstractFilteredContainerWrapper extends EventDispatcher implements IOContainer
	{


		////////////////////////////////////////////////////
		// target
		////////////////////////////////////////////////////
		
		protected var _target:IOContainer;
		
		

		////////////////////////////////////////////////////
		// filter
		////////////////////////////////////////////////////
		
		private var filter:Function;


		private var filterCache:Dictionary;



		////////////////////////////////////////////////////
		// etc...
		////////////////////////////////////////////////////


		/**
		 * 
		 * The difference with the target such that
		 * this.size + filteredElementsCount = target.size.
		 * 
		 * We keep this property in sync all the time
		 * 
		 * 
		*/
		private var filteredElementsCount:int = 0;




		public function get isEmpty( ):Boolean
		{
			return size == 0;
		}




		public function get size():uint
		{
			return _target.size - filteredElementsCount;
		}





		/**
		 * 
		 * @param target The set that should be filtered
		 * 
		 * @param filter
		 * 		The function called to filter elements. It takes the element as a parameter and returns true ( filter passed )
		 * 		or false ( filter failed ).
		 * 
		 * 
		 * @param cacheFilterResult
		 * 		Wether the results of the function should be cached.
		 * 
		 */
		public function AbstractFilteredContainerWrapper( target:IOContainer, filter:Function, cacheFilterResult:Boolean = false )
		{
	
			_target = target;
			
			this.filter = filter;			
			
			if ( cacheFilterResult )
				filterCache = new Dictionary( true );
			
			
			if ( _target.size > 0 ) // we only sync if the target is not empty
			{
				syncFilteredElementsCount( );
			}
			
				
			_target.addEventListener( DataStructureEvent.CHANGE, target_change, false, 0, true );
				
		}
		
		
		
		
		/**
		 * 
		 * We intercept the events coming from our target and create our own, slightly
		 * modified versions
		 * We also update filteredElementsCount
		 * 
		 * @param event
		 * 
		 */		
		private function target_change( event:DataStructureEvent ):void
		{
			
			var diff:ContainerDiff, mustDispatch:Boolean, mcs:Dictionary, mcs2:Dictionary;
			
			diff = event.diff as ContainerDiff;
			
			mustDispatch = hasEventListener( DataStructureEvent.CHANGE );
			
			// original multiplicity changes
			mcs = diff.multiplicityChanges;
			
			
			if ( mustDispatch ) // optimize
			{
				// filtered multiplicity changes ( to be filled )
				mcs2 = new Dictionary( );
			}



			// inspect multiplicity changes
			
			for ( var e:* in mcs )
			{
				
				var mc:MultiplicityChange = diff.multiplicityChanges[ e ];
				
				if ( test( e ) ) // OK: pass thru
				{
					if ( mustDispatch ) // optimize
						mcs2[ e ] = mc;					
				}
				else // FAIL: update filteredElementsCount
				{
					filteredElementsCount += mc.delta;			
				}
			
			}	
			
			
			// dispatch our version of the event

			if ( mustDispatch ) // optimize
			{
			
				var diff2:ContainerDiff = new ContainerDiff( mcs2 );
				dispatchEvent( new DataStructureEvent( DataStructureEvent.CHANGE, diff2 ) );
			}
			
			
		}
		
		
		
		
		
		
		
		
		
		
		private function test( e:* ):Boolean
		{
			
			if ( filterCache ) // cache is activated
			{
				if ( filterCache[ e ] === undefined ) // not cached yet
				{
					filterCache[ e ] = Boolean( filter.call( null, e ) );
				}			
				
				return Boolean( filterCache[ e ] );
			}
			
			return Boolean( filter.call( null, e ) );		
		}
		
		
		
		
		
		
		/**
		 * 
		 * 
		 * 
		 */
		private function syncFilteredElementsCount( ):void
		{
			filteredElementsCount = 0;
			// we walk our target to determine the amount of elements that are filtered out
			_target.walk( syncWalker );
		}
		
		
		
		
		
		private function syncWalker( e:*, m:int ):void
		{
			if ( ! test ( e ) )
			{
				filteredElementsCount += m;			
			}
		}
		
		
		
		
		
		
		public function contains( e:* ):Boolean
		{
			if ( ! _target.contains( e ) )
				return false;
				
			return test( e );			

		}
		



		public function getMultiplicity( e:* ):uint
		{
			if ( ! test( e ) )
			{
				return 0;
			}
				
			return _target.getMultiplicity( e );
		}

		

		public function any( emptyValue:* = undefined ):*
		{
			return _target.any( emptyValue );
		}


		public function get asArray( ):Array
		{
			return _target.asArray;
		}
		
		
		
		public function walk( walker:Function ):void
		{
			
			_target.walk( function( e:*, m:int ):void {
					
					if ( test( e ) )
					{
						walker.apply( null, [ e, m ] );
					}
				
				} );			
			
		}
		


		
		public function compare( other:* ):ComparisonResult
		{
		
			return ContainerUtil.doCompare( this, other );
		
		}




		
	}
}